feat(overlay): add keyboard dispatcher for targeting correct overlay#6682
Conversation
| return this._detachments.asObservable(); | ||
| } | ||
|
|
||
| /** Returns an observable that emits when the overlay has been disposed. */ |
There was a problem hiding this comment.
nit: prefer "Gets" to "Returns"
| * Subscribe to attach and detach events and register them accordingly | ||
| * with the keyboard dispatcher service. | ||
| */ | ||
| private _trackOverlayAttachments(overlayRef: OverlayRef): void { |
There was a problem hiding this comment.
Could each OverlayRef to this itself in its constructor?
|
@jelbourn comments addressed. Also added a demo to the overlay. |
| RxChain.from(overlayRef.keyboardEvents()) | ||
| .call(filter, event => event.type === 'keydown' && event.keyCode === ESCAPE) | ||
| .subscribe(() => { | ||
| if (!dialogRef.disableClose) { |
There was a problem hiding this comment.
Might be a bit more concise to have this in filter, instead of checking it in the subscribe.
| const bodyKeyboardEvents = merge( | ||
| fromEvent(document.body, 'keydown'), | ||
| fromEvent(document.body, 'keypress'), | ||
| fromEvent(document.body, 'keyup')) as Observable<KeyboardEvent>; |
There was a problem hiding this comment.
Instead of casting it to an Observable<KeyboardEvent>, you can pass the KeyboardEvent as a generic: merge<KeyboardEvent>.
| private _dispatchKeyboardEvents(): void { | ||
| const bodyKeyboardEvents = merge( | ||
| fromEvent(document.body, 'keydown'), | ||
| fromEvent(document.body, 'keypress'), |
There was a problem hiding this comment.
I'm not sure that this should handle keydown, keypress and keyup @jelbourn. Since for 99% of cases you'd only need keydown, it means that every time you consume the service, you'd have to have a filter(event => event.type === 'keydown').
There was a problem hiding this comment.
I'd be happy with this. I was aiming for generic, but the api could be "rebranded" 'to just keydownEvents.
| /** Select the appropriate overlay from a list of overlays and a keyboard event. */ | ||
| private _selectOverlayFromEvent(overlays: OverlayRef[], event: KeyboardEvent): OverlayRef { | ||
| // Check if any overlays contain the event | ||
| const targetedOverlay = this._attachedOverlays.find(overlay => |
There was a problem hiding this comment.
This doesn't handle if the keyboard event came directly from the overlay element. You can tweak the predicate to include overlay.overlayElement === event.target.
| } | ||
|
|
||
| /** Select the appropriate overlay from a list of overlays and a keyboard event. */ | ||
| private _selectOverlayFromEvent(overlays: OverlayRef[], event: KeyboardEvent): OverlayRef { |
There was a problem hiding this comment.
Since the overlays is always going to be this._attachedOverlays, you can take it from the class property instead of passing it in.
| private _detachments = new Subject<void>(); | ||
| private _disposed = new Subject<void>(); | ||
|
|
||
| _dispatchedKeyboardEvents = new Subject<KeyboardEvent>(); |
There was a problem hiding this comment.
Instead of having the OverlayRef add and remove itself from the dispatcher, a cleaner approach would be to only have it expose the stream of keyboard events. Then the OverlayRef.keyboardEvents can be simplified to:
// note: don't get too caught up in the naming
return this.keyboardDispatcher.events.filter(event => event.target === this.overlayElement);This leaves less room for mistakes when managing the overlays and it also removes the circular dependency between the OverlayRef and the dispatcher.
Edit: While we're at it, this could avoid having to add a dispatcher in the first place. The OverlayRef can bind the keyboard listener on its own and only filter out relevant events.
There was a problem hiding this comment.
Hm I don't entirely follow.
- I kind of see how I could remove the
Subject; maybe if I created a new event type that paired overlays to keydowns, and then the overlay just filters out ones directed at itself and maps to the keydown. - An important piece of this is dispatching an event to the most recently attached overlay. Often, events land on the body with no other context. That's the whole point of tracking overlays as they are attached and detached.
There was a problem hiding this comment.
Alright, I see. I guess that in most cases the top-most overlay would be the one dispatching the keyboard events, but that won't always be the case (like with md-autocomplete). Let's keep it.
There was a problem hiding this comment.
One of the original problems was that if you opened a dialog without focusable elements, focus would still be on trigger or wherever it was before, but I see that's since been fixed. This plunker from the original issue shows that clicking on the backdrop still moves focus to the body.
I'll go ahead with the other changes and we can make sure it all still makes sense.
bdf8ca7 to
e074fa0
Compare
|
@crisbeto comments addressed. Please take another look! |
crisbeto
left a comment
There was a problem hiding this comment.
Mostly LGTM, a few minor notes left.
| private _selectOverlayFromEvent(event: KeyboardEvent): OverlayRef { | ||
| // Check if any overlays contain the event | ||
| const targetedOverlay = this._attachedOverlays.find(overlay => { | ||
| return overlay.overlayElement.contains(event.target as HTMLElement) || |
There was a problem hiding this comment.
I'd move the overlay.overlayElement === event.target check first since it could potentially exit sooner.
| }); | ||
|
|
||
| // Use that overlay if it exists, otherwise choose the most recently attached one | ||
| return targetedOverlay ? |
There was a problem hiding this comment.
return targetedOverlay || this._attachedOverlays[this._attachedOverlays.length - 1].
| private _attachments = new Subject<void>(); | ||
| private _detachments = new Subject<void>(); | ||
|
|
||
| _dispatchedKeydownEvents = new Subject<KeyboardEvent>(); |
There was a problem hiding this comment.
Add a description to this one. Also potentially rename it to something like _keydownEvents.
|
@willshowell looks like the |
|
@crisbeto I had issues with |
crisbeto
left a comment
There was a problem hiding this comment.
Sorry, but one final nitpick.
| * Subscribe to keydown events that land on the body and dispatch those | ||
| * events to the appropriate overlay. | ||
| */ | ||
| private _dispatchKeydownEvents(): void { |
There was a problem hiding this comment.
Judging by the name of the method, I'd assume that it would dispatch the events immediately, but that's not what it does. Something along the lines of _subscribeToKeydownEvents might be more appropriate.
b1e56ac to
e90c917
Compare
241cf71 to
95f9016
Compare
|
Rebased. cc @kara |
|
@kara is there anything I need to do regarding the presubmit failure? |
95f9016 to
a3d3536
Compare
|
Getting some AOT issue: Error: Error encountered resolving symbol values statically. Only initialized variables and constants can be referenced because the value of this variable is needed by the template compiler (position 10:22 in the original .ts file), resolving symbol DOCUMENT in /platform-browser/src/dom/dom_tokens.d.ts, resolving symbol DOCUMENT in /platform-browser/src/platform-browser.d.ts, resolving symbol DOCUMENT in /platform-browser/public_api.d.ts, resolving symbol DOCUMENT in /platform-browser/index.d.ts, resolving symbol DOCUMENT in platform_browser_safe/index.d.ts, resolving symbol OVERLAY_KEYBOARD_DISPATCHER_PROVIDER in /src/cdk/overlay/keyboard/overlay-keyboard-dispatcher.ts, resolving symbol OVERLAY_PROVIDERS in /src/cdk/overlay/overlay-module.ts, resolving symbol OverlayModule in /src/cdk/overlay/overlay-module.ts, resolving symbol OverlayModule in /src/cdk/overlay/overlay-module.ts Can you try building in AOT? |
a3d3536 to
2034dfc
Compare
|
@andrewseguin I wasn't able to reproduce, but I've rebased and updated the license. I'm passing on |
|
Is injecting |
|
@mmalerba we should only need to use |
|
I think |
|
Oh yeah, now I remember. I had to do the same thing for the tests for {provide: DIR_DOCUMENT, useExisting: DOCUMENT}, |
2034dfc to
bbbf598
Compare
|
Doesn't seem there's any need to mock |
|
@mmalerba should be ready for another spin |
After the switch to `ActiveDescendantKeyManager`, the select was no longer handling the escape key functionality internally, but was delegating to the underlying overlay. Since the overlay listens to keyboard events on the document, it means that escape key presses will close any parent overlays along the way. These changes add the escape listener to the select itself and stop the event propagation. **Note:** the overlay directive listener should be scoped to the overlay itself, but it's better if we defer refactoring it until angular#6682 gets in. Fixes angular#7981.
bbbf598 to
43c8490
Compare
Fix typo and add comment Address comments Add keyboard tracking demo to overlay Address comments Revert no-longer-needed dispose changes Address comments Fix prerender error by lazily starting dispatcher Address naming comment
|
This issue has been automatically locked due to inactivity. Read more about our automatic conversation locking policy. This action has been performed automatically by a bot. |
Fixes #6330
Fixes #3251
Closes #3460